Vue3 基础 – 计算属性 & 侦听器 & 样式绑定 & 模板引用

2023年 7月 9日

内容纲要

1.1 setup 函数1.2 组合式 API1. reactive 函数2. 修改 reactive 数据3. ref 函数4. 修改 ref 数据5. 将对象赋值给 ref2.1 基于模板中的表达式统计商品信息2.2 计算属性的基本语法2.3 基于计算属性实现加法运算2.4 基于计算属性改造购物车案例2.5 计算属性的缓存 vs 方法3.1 侦听器的基本语法3.2 停止侦听器3.3 侦听的数据源类型1. 侦听 ref 数据源2. 侦听 reactive 数据源3. 侦听 reactive 中某个属性的变化(属性值为对象)4. 基于 getter 侦听 reactive 中某个属性的替换操作(属性值为对象)5. 基于 getter 侦听 reactive 中某个值类型数据的变化6. 侦听多个数据源组成的数组3.4 深层侦听器3.5 即时回调的侦听器3.6 回调的触发时机3.7 watchEffect()1. watch 侦听器的缺点2. watchEffect()3. watch() vs watchEffect()4.1 绑定 HTML 的 class1. 绑定单个 class2. 动态切换单个 class3. 基于计算属性动态切换单个 class4. 绑定 class 对象5. 把 class 对象封装为 reactive 响应式数据6. 把 class 对象封装为 computed 计算属性7. 绑定 class 数组4.2 绑定 style 内联样式1. 绑定对象2. 绑定计算属性3. 绑定数组5.1 访问模板引用5.2 v-for 中的模板引用5.3 函数模板引用

版权归作者 ©刘龙宾 所有,本文章未经作者允许,禁止私自转载!

1. setup 函数 & 组合式 API

在前面的课程中,我们曾强调过:使用 data() 函数声明响应式的数据、在 methods 节点下声明事件处理器,都是 Vue2 中的旧语法(选项式API)。这种旧语法的存在,主要是为了防止断崖式升级造成的老用户流失。

在 Vue3 中,推出了组合式 API 的新概念,它可以极大提高功能模块的封装性复用性。因此,官方推荐所有用户优先使用组合式 API。

在今后的开发中,为了维护老项目,我们可能会不得不用到选项式 API。除此之外,在开发新项目时,推荐大家优先使用组合式 API。

1.1 setup 函数

setup() 函数是 Vue3 中组合式 API 的入口,它的语法格式如下:

例如,在 setup return 的对象中,声明一个数据 name

紧接着,在模板中即可使用名为 name 的数据了:

注意:刚才定义的 name,并非是响应式的数据,所以基于 app._instance.proxy.name 修改它的值后,不会触发视图的重新渲染。

1.2 组合式 API

组合式 API 是 Vue3 提供的一系列函数。例如,可以使用 reactive()ref() 这两个函数,来声明响应式的数据。

1. reactive 函数

reactive 函数用来定义响应式的数据对象,它的使用步骤如下:

接下来,就可以在模板中使用响应式的数据 user 啦:

2. 修改 reactive 数据

如果想要点击按钮,修改 user.name 的值,我们可以在 setup 中定义事件的处理函数,并挂载到 return 的对象上,供模板使用:

最后,在模板中,为 <button> 按钮绑定点击事件:

reactive 函数的缺点:只能为引用类型的数据创建响应式数据对象,无法将值类型的数据转化为响应式数据。

3. ref 函数

ref 函数用来将值类型的数据转化为响应式数据,它的基本用法如下:

紧接着,就可以在模板中使用 count 了:

4. 修改 ref 数据

如果想要点击按钮,修改 count 的值,则需要在 setup 函数中定义事件的处理函数,并挂载到 return 的对象上,供模板使用:

最后,在模板中,为 <button> 按钮绑定点击事件:

注意:使用 ref 声明的响应式数据,在 js 中必须使用 .value 进行访问。在模板中不必使用 .value 进行访问,因为在模板中使用时会自动对 ref 数据进行解包。

5. 将对象赋值给 ref

ref 函数除了接收值类型的数据之外,还可以将对象类型的数据转化为响应式数据。例如:

在模板中,直接使用 info 即可,因为模板中会对 ref 数据自动解包:

注意:如果 ref() 处理的是对象类型的数据,则内部会调用 reactive() 函数将对象数据转为响应式数据,然后挂载为 .value 属性。

最佳实践:尽量使用 ref() 来定义响应式数据,因为它既可以处理值类型数据,又可以处理对象类型的数据。

2. 计算属性

计算属性可以避免程序员在模板中编写大段的 JavaScript 表达式,从而提高代码的整洁度和可维护性。

2.1 基于模板中的表达式统计商品信息

  1. 定义购物车中的商品数据如下:

  2. 在模板中渲染购物车中的商品数据,并基于模板中的表达式计算商品的数量、总价、运费:

  3. 编写样式,为商品添加分割线:

  4. 在步骤2中,我们发现模板中掺杂了较为复杂的表达式;而且在商品总价配送费中,两次用到了商品的总价,但每次都需要重新计算总价,无法实现数据的复用。

    基于这样的原因,我们可以利用 computed 计算属性,把模板中的表达式运算,封装到 JS 中,从而达到简化模板实现数据复用的目的。

2.2 计算属性的基本语法

  1. Vue 对象上解构出 computed 函数,它用来定义计算属性:

  2. 调用 computed 函数,同时向 computed 中传递一个箭头函数作为参数:

    而且,在箭头函数中,必须 return 一个计算的结果。如果忘记了 return,则是一个无效的计算属性。

  3. computed 函数的返回值,是一个 ref 的响应式对象,可通过 .value 访问其值。如果在模板中访问,会自动进行解包:

2.3 基于计算属性实现加法运算

  1. 通过 ref 定义响应式数据 n1n2,通过 computed 定义计算属性 result

  2. 在模板中使用计算属性:

    注意:当计算属性依赖的响应式数据发生变化时,会自动对计算属性重新求值。

2.4 基于计算属性改造购物车案例

  1. 定义购物车中的商品数据如下:

  2. 分析模板的结构,我们可以得到如下的结论:

    • 商品的数量,是购物车中所有商品的数量总和,依赖于每个商品数量的变化,是一个派生属性
    • 商品的总价,是购物车中所有商品的单价×数量的总和,依赖于每个商品的数量和单价的变化,是一个派生属性
    • 商品的运费,依赖于商品总价的变化而变化,也是一个派生属性

    所以,我们可以基于计算属性,分别计算出以上3个数据的值,代码如下:

  3. 在模板中,使用使用这三个计算属性的值即可:

    思考:如果在模板中,想要把实际的支付金额(商品总价 + 运费)显示出来,用计算属性该如何实现?

2.5 计算属性的缓存 vs 方法

我们发现,如果把计算属性的业务逻辑封装到 function 中,也能实现相同的结果,例如计算两个数之和的案例:

接下来,在模板中调用这个方法,显示计算的结果:

注意:使用 function 并不会缓存计算的结果。当我们在模板中多次调用这个 function 时,每次使用都会触发的的执行,哪怕每次计算的结果都是一模一样的:

所以 function 总是会在每次调用时重新执行计算的过程,不会对计算的结果进行缓存。

而 computed 计算属性就很好的实现了缓存的功能:当计算属性中所依赖的响应式数据没有发生变化时,多次使用计算属性,会立即返回之前计算的结果,这极大的提高了计算的性能。

计算属性的优点:1. 能缓存计算的结果 2. 能简化模板的代码

3. 侦听器

侦听器允许我们监视响应式数据的变化,并触发回调函数的执行,在回调函数的形参中,可以接收 newValueoldValue 两个形参,分别代表变化后的新值变化前的旧值

3.1 侦听器的基本语法

  1. Vue 对象上解构出 watch 函数:

  2. setup 函数中,调用 watch 函数,声明侦听器:

  3. 例如:

    对应的模板为:

3.2 停止侦听器

调用 watch 函数会创建一个侦听器,并返回一个终止侦听器的函数,语法格式如下:

我们可以改造一下前面的例子,当 count >= 20 时,我们希望停止侦听器的执行,并且移除本地存储,代码如下:

3.3 侦听的数据源类型

watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组。

1. 侦听 ref 数据源

2. 侦听 reactive 数据源

3. 侦听 reactive 中某个属性的变化(属性值为对象)

  1. 定义 reactive 类型的响应式数据 obj,其中 obj.info 是一个对象:

  2. 如果想侦听 obj.info 下每个属性的变化,并得到变化后的新 obj.info 对象,则可以使用 watch 进行如下的侦听:

  3. 当我们修改 obj.info.name 的值,或修改 obj.info.age 的值,都会触发此侦听器。得到的 newValue 是变化后新的 obj.info 对象。

4. 基于 getter 侦听 reactive 中某个属性的替换操作(属性值为对象)

  1. 定义 reactive 类型的响应式数据 obj,其中 obj.info 是一个对象:

  2. 如果想侦听 obj.info 对象的替换操作,例如,在定时器中把 obj.info 替换为一个新对象:

  3. 此时,使用 watch(obj.info, 回调函数) 无法侦听到对象的替换操作。必须使用 getter 才能侦听到对象的替换操作

    注意:所谓的 getter,指的就是通过箭头函数来访问响应式对象中的某个具体属性的取值操作。

5. 基于 getter 侦听 reactive 中某个值类型数据的变化

  1. 定义 reactive 类型的响应式数据 obj,其中 obj.info 是一个对象:

  2. 如果想侦听 obj.count 的变化,则不能使用 watch(obj.count, 回调函数) 的方式进行侦听。因为 obj.count 返回的是具体的值,不是响应式的数据。因此,必须使用 getter 才能侦听到对象中值类型数据的变化:

6. 侦听多个数据源组成的数组

  1. 定义 refreactive 类型的响应式数据如下:

  2. 调用 watch 声明侦听器时,提供一个要侦听的数据源的数组,当其中任何一个数据发生变化时,都会触发回调函数的执行:

3.4 深层侦听器

  1. 假设有如下的响应式数据:

  2. 如果定义了 watch(obj.info, 回调函数) 侦听器,则只能侦听到 obj.info.nameobj.info.age 的修改操作,无法侦听到 obj.info 对象的替换操作。

  3. 如果定义了 watch(() => obj.info, 回调函数) 侦听器,则只能侦听到 obj.info 对象的替换操作,无法侦听到某个具体属性的修改操作。也就是说,目前的情况下鱼和熊掌不可兼得。

  4. 如果既想侦听到对象的替换操作,又想侦听到对象下某个具体属性的修改操作。此时可以给 watch 侦听器提供第3个参数,它是一个配置对象,通过 deep: true 选项即可实现我们的目的,语法格式如下:

3.5 即时回调的侦听器

watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。

此时,我们可以通过传入 immediate: true 选项来强制侦听器的回调立即执行:

3.6 回调的触发时机

当你更改了响应式状态,它可能会同时触发 Vue 组件更新侦听器回调

默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。

如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: 'post' 选项:

例如,在 watch 侦听器中,如果想得到最新的 DOM 内容,则需要添加 flush: post 选项,否则得到的是 DOM 更新前的旧内容:

对应的模板结构为:

3.7 watchEffect()

1. watch 侦听器的缺点

watch 侦听器需要手动维护依赖的列表,如果侦听器所依赖的数据很多,则维护依赖列表的成本会很高,例如:

我们发现,只要 p1, p2, p3 任何一个发生变化,都会触发 watch 的回调函数,而在回调函数中,我们又把这3个数据当做调用接口的参数

在使用 watch 的时候,我们不得不维护一个 [p1, p2, p3] 的数组,从而明确告诉 watch 要侦听哪些数据的变化。如果要侦听的数据项很多,则每次添加或删除了调用接口的参数时,都需要手动维护这个数组中的元素,非常的麻烦。

例如:下面的例子中,忘记了把 p4 添加到侦听器的依赖数组中,最终导致 p4 变化时,无法触发侦听器的执行:

2. watchEffect()

为了让用户不必维护侦听器的依赖数组,Vue3 提供了 watchEffect() 函数,它会自动收集响应式数据的依赖关系,例如:

另外,watchEffect() 中的回调函数会立即执行,因此不需要指定 immediate: true

在第一次执行期间,他会自动收集 p1.value, p2.value, p3.value, p4.value 作为依赖。

每当任何一个响应式数据发生变化时,回调函数会再次执行。

因此 watchEffect 让我们不用再明确传递触发回调执行的依赖项

3. watch() vs watchEffect()

相同点:

watch 和 watchEffect 都能侦听响应式数据的变化,并重新执行给定的回调函数。

不同点:

4. 类与样式绑定

4.1 绑定 HTML 的 class

每个 HTML 元素都支持使用 class 属性来美化元素的样式,因此 v-bind 指令同样适用于元素的 class 属性,我们可以为 class 动态绑定类名,从而达到动态修改元素样式的目的。

为了演示如何绑定 HTML 的 class,我们先定义如下的一组 class 样式:

1. 绑定单个 class

定义 ref 响应式数据 cname 并把它暴露给模板:

在模板中,通过 :class="cname" 为元素动态绑定 class 类名:

点击 button 按钮时,给 cname 赋值:

注意:在给元素提供 class 的时候,可以把静态 class动态绑定的 class 分别应用于同一个元素。

2. 动态切换单个 class

需求:点击按钮,动态为元素切换 thinfat 的 class 类名。

首先,定义布尔值 flag 并暴露给模板使用:

其次,实现点击按钮控制布尔值的切换:

最后,根据布尔值的状态,为元素动态绑定 class 类名:

注意:这里用到了三元表达式,为了模板内容的简洁,我们也可以把三元表达式封装为 computed 计算属性。

3. 基于计算属性动态切换单个 class

定义 cname 的计算属性:

在模板中把 cname 动态绑定为元素的 class

4. 绑定 class 对象

为元素动态绑定 class 对象,可以控制切换元素的多个 class,它的语法格式如下:

例如下面的例子:

定义 ref 类型的响应式数据 isRedisBgYellow,它们的默认值为为布尔值 false

在模板中,点击按钮切换这两个布尔值的状态:

在模板中,为 h1 元素绑定 class 对象:

5. 把 class 对象封装为 reactive 响应式数据

为了简化模板中的代码,我们还可以把动态绑定的 class 对象,封装为 reactive 类型的响应式数据对象:

在模板中,为 h1 元素绑定 class 对象:

并且在点击按钮的时候,动态切换 classObj.redclassObj.bgyellow 的布尔值,从而决定是否把 class 类名应用给元素:

6. 把 class 对象封装为 computed 计算属性

除了可以把动态绑定的 class 对象封装为 reactive 类型的响应式数据之外,我们还可以把它封装为 computed 计算属性,这也能简化模板中绑定 class 的代码:

在模板中,为 h1 元素动态绑定 class 对象:

点击按钮时,动态切换布尔值的状态:

7. 绑定 class 数组

我们可以为 class 绑定数组,从而为元素应用多个 class:

也可以通过三元表达式,动态控制某个 class:

还可以把三元表达式改造成对象的形式,按需为元素应用 class:

为了进一步简化模板中的代码,我们还可以把数组封装为 computed 计算属性:

然后,只需要为元素的 class 绑定这个计算属性即可:

4.2 绑定 style 内联样式

1. 绑定对象

:style 支持绑定 JavaScript 的对象值。例如,定义一个 ref 类型的响应式数据:

在模板中为 h1 绑定 style 样式对象:

注意:如果是连字符格式的样式属性,推荐写成小驼峰的形式。

2. 绑定计算属性

为了保证模板的简洁,我们可以把绑定给 style 的样式对象,封装为 computed 计算属性:

在模板中,直接使用计算属性:

3. 绑定数组

我们还可以给 :style 绑定一个包含多个样式对象的数组。这些对象会被合并后渲染到同一元素上。例如,声明如下的两个 style 样式对象:

styleObjstyleObj2 以数组的形式,绑定为 h1style 样式:

5. 模板引用

虽然 Vue 的声明性渲染模型为你抽象了大部分对 DOM 的直接操作,但在某些情况下,我们仍然需要直接访问底层 DOM 元素。要实现这一点,我们可以使用特殊的 ref attribute:

5.1 访问模板引用

那么,在 JS 中如何才能够访问到模板中的元素呢?这就需要我们使用 ref() 函数声明一个同名的响应式数据:

5.2 v-for 中的模板引用

当在 v-for 中使用模板引用时,对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素:

v-for 循环生成的 li 添加 ref 属性:

在 JS 中使用 ref() 函数声明同名的数据:

5.3 函数模板引用

除了使用字符串值作名字,ref attribute 还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:

注意我们这里需要使用动态的 :ref 绑定才能够传入一个函数。当绑定的元素被卸载时,函数也会被调用一次,此时的 el 参数会是 null。你当然也可以绑定一个组件方法而不是内联函数。

例如,在定义响应式数据 flag,用来控制页面上 input 元素的显示和隐藏:

在模板中,通过点击按钮,切换 flag 的值:

最后,使用 v-if 指令控制 input 的显示和隐藏,并给 input 添加函数模板引用